home *** CD-ROM | disk | FTP | other *** search
Text File | 1996-12-07 | 43.9 KB | 1,415 lines |
- program WorkinTooHard;
-
- (* WorkinTooHard *)
- (* ©1996 Quinn "The Eskimo!" *)
-
- (* Things to note:
-
- o This program uses the term Tick and Tock in non-obvious ways. A Tick
- is the basic unit of time, in this case a minute. A Tock is a collection
- of Ticks, typically about fifteen minutes. The program wakes up every
- Tick to sample the mouse position to determine whether there has been
- activity. Every fifteen Ticks, the program Tocks, and records the
- proportion of Ticks that occured with user activity since the last Tock.
- Do not confuse Ticks with the 60th of a second time measurement used
- by the Mac system software.
-
- o Unlike most Mac programs, this one saves it's preferences in its own
- resource fork. Yeah, I know it's bad style, but it sure makes the code
- easier. Besides, this is a tiny little desktop utility application,
- so where's the harm in that?
- *)
-
- uses
- Types,
- Errors,
- QuickDraw,
- Fonts,
- Windows,
- Menus,
- TextEdit,
- Dialogs,
- Memory,
- Resources,
- GestaltEqu,
- QDOffscreen,
- Devices,
- TextUtils,
- AppleEvents,
- Scrap,
- IntlResources,
- Icons,
- Power;
-
- (* ***** Resource Constants ***** *)
-
- const
- (* MBAR *)
- rMainMenuBar = 128;
- (* WIND *)
- rMainWindow = 128;
- (* RGB# *)
- rColourMap = 128;
- (* MENU *)
- mApple = 128;
- iAbout = 1;
- mFile = 129;
- iSave = 1;
- (* - *)
- iQuit = 3;
- mEdit = 130;
- iUndo = 1;
- (* - *)
- iCut = 3;
- iCopy = 4;
- iPaste = 5;
- iClear = 6;
- mDebug = 131;
- iGrow = 1;
- iShrink = 2;
- (* - *)
- iTimeBackward = 4;
- iTimeForward = 5;
- (* - *)
- iFail = 7;
- (* ALRT *)
- rAboutAlert = 256;
- rInsufficientlyCoolAlert = 257;
- (* PRef *)
- rWindowPosition = 128;
- rSavedActivity = 129;
- (* STR# *)
- rMiscStrings = 128;
- strTimeDiscontinuity = 1;
- strPatience = 2;
- strDeath = 3;
- (* ics# *)
- rSunIcon = 256;
- rMoonIcon = 257;
-
- (* ***** Toolbox Utilities ***** *)
-
- procedure QAssert(mustBeTrue : Boolean);
- forward;
-
- procedure InitToolbox;
- begin
- InitGraf(@qd.thePort);
- InitFonts;
- InitWindows;
- InitMenus;
- TEInit;
- InitDialogs(nil);
- MaxApplZone;
- MoreMasters;
- MoreMasters;
- MoreMasters;
- end; (* InitToolbox *)
-
- function GetWindowContentRegion (theWindow: WindowPtr): RgnHandle;
- (* Returns the window's content region. This is the region currently
- being used, not a copy. Do not munge it!
- *)
- begin
- GetWindowContentRegion := WindowPeek(theWindow)^.contRgn;
- end; (* GetWindowContentRegion *)
-
- function GetWindowStructureRegion (theWindow: WindowPtr): RgnHandle;
- (* Returns the window's structure region. This is the region currently
- being used, not a copy. Do not munge it!
- *)
- begin
- GetWindowStructureRegion := WindowPeek(theWindow)^.strucRgn;
- end; (* GetWindowStructureRegion *)
-
- function TitleBarOnScreen (theWindow: WindowPtr): Boolean;
- (* Returns true if the window's title bar is on the screen.
- Note that this routine only works if the window is visible,
- ie you have called ShowWindow on it. The standard mechanism
- for using this routine is to ShowWindow the window, then
- call TitleBarOnScreen. If it returns true, everything is cool.
- Otherwise the window is completely off the screen, so you can
- move it back on without causing visible effects.
- *)
- var
- result : Boolean;
- titleBarRegion: RgnHandle;
- begin
- result := true;
- titleBarRegion := NewRgn;
- if titleBarRegion <> nil then begin
- (* First calculate the title bar region by subtracting the content
- region away from the structure region.
- *)
- CopyRgn(GetWindowStructureRegion(theWindow), titleBarRegion);
- DiffRgn(titleBarRegion, GetWindowContentRegion(theWindow), titleBarRegion);
-
- (* Now intersect the title bar region with the grey region, ie the region
- describing the extent of the desktop and return true if the intersection
- is not empty.
- *)
- SectRgn(titleBarRegion, GetGrayRgn, titleBarRegion);
- result := not EmptyRgn(titleBarRegion);
- DisposeRgn(titleBarRegion);
- end; (* if *)
- TitleBarOnScreen := result;
- end; (* TitleBarOnScreen *)
-
- procedure GetWindowRect (theWindow: WindowPtr; var windowRect: Rect);
- (* This routine sets windowRect to the global co-ordinates of
- the position of the window. It's typically used for saving window
- state.
- *)
- var
- oldPort : GrafPtr;
- begin
- GetPort(oldPort);
- SetPort(theWindow);
- windowRect := WindowPeek(theWindow)^.port.portRect;
- LocalToGlobal(windowRect.topLeft);
- LocalToGlobal(windowRect.botRight);
- SetPort(oldPort);
- end; (* GetWindowRect *)
-
- function DateTimeToString(dateTime : longint) : Str255;
- (* Returns a string that is the concatination of the
- date and the time, both derived from the dateTime
- parameter.
- *)
- var
- result : Str255;
- tmpStr : Str255;
- begin
- IUDateString(dateTime, abbrevDate, result);
- IUTimeString(dateTime, true, tmpStr);
- result := concat(result, ', ', tmpStr);
- DateTimeToString := result;
- end; (* DateTimeToString *)
-
- function AEGotRequiredParams (theAppleEvent: AppleEvent): OSStatus;
- (* Returns no error if you've extracted all of the required
- parameters out of the AppleEvent.
- *)
- var
- typeCode: DescType;
- actualSize: Size;
- err: OSStatus;
- begin
- err := AEGetAttributePtr(theAppleEvent, keyMissedKeywordAttr, typeWildCard, typeCode, nil, 0, actualSize);
- if err = errAEDescNotFound then begin
- err := noErr;
- end else if err = noErr then begin
- err := errAEEventNotHandled;
- end; (* if *)
- AEGotRequiredParams := err;
- end; (* AEGotRequiredParams *)
-
- function GetShortDayName(dayOfWeek : integer) : Str255;
- (* Parses the international resources to return the
- abbreviated text representation of the dayOfWeek
- parameter.
- *)
- type
- Itl1ExtRecPtr = ^Itl1ExtRec;
- Itl1ExtRecHandle = ^Itl1ExtRecPtr;
- var
- itl1H : Handle;
- result : Str255;
- abbrevTableBase : Ptr;
- i : integer;
- begin
- itl1H := GetResource('itl1', GetScriptManagerVariable(smScriptDate));
- QAssert( itl1H <> nil );
- QAssert( GetHandleSize(itl1H) >= sizeof(Itl1ExtRec) );
-
- abbrevTableBase := Ptr( longint(itl1H^) + Itl1ExtRecHandle(itl1H)^^.abbrevDaysTableOffset + sizeof(integer));
-
- for i := 1 to dayOfWeek - 1 do begin
- abbrevTableBase := Ptr( longint(abbrevTableBase) + abbrevTableBase^ + 1 );
- end; (* for *)
-
- BlockMoveData(abbrevTableBase, @result, sizeof(result));
-
- GetShortDayName := result;
- end; (* GetShortDayName *)
-
- (* ***** Application Types, Constants and Global Data ***** *)
-
- (* ----- Debugging ----- *)
-
- var
- gDebugging : Boolean;
- const
- kOptionKeyCode = 58;
-
- (* gDebugging is a pseudo-constant, established at startup time, that
- enables/disables the built-in debugging in the program. The value
- is set depending on the state of the kOptionKeyCode at startup
- time.
- *)
-
- (* ----- Time Related Constants ----- *)
-
- var
- gSecondsPerTick : longint;
- const
- kDebuggingSecondsPerTick = 1;
- kStandardSecondsPerTick = 60;
-
- (* gSecondsPerTick is another pseudo-constant that defines the number of
- real seconds that make up a Tick. At startup time it is set to
- either kDebuggingSecondsPerTick or kStandardSecondsPerTick depending
- on whether we're debugging. Changing this constant effectively
- makes time run a lot faster, although some weird things happen,
- such as weird placement of the day and night icons.
- *)
-
- const
- kNumberOfDays = 6;
- kTicksPerTock = 15;
- kTocksPerHour = 60 div kTicksPerTock;
- kNumberOfTocks = kNumberOfDays * 24 * kTocksPerHour;
-
- (* The above constants determine important timing parameters for
- the program. kNumberOfDays is the number of days records
- that the program keeps. You can change it at will, although
- the window size will start to grow unpleasantly. kTicksPerTock
- is the number of Ticks in a Tock. Currently we Tock every
- 15 Ticks, ie every quarter hour. kTocksPerHour is the number
- of Tocks we log per hour. Note that this constant doesn't factor
- in gSecondsPerTick because we want the changing of gSecondsPerTick
- to accellerate time. kNumberOfTocks is the total number of Tocks
- that we log. Ipso facto, it's the number of pixels across in the
- window.
- *)
-
- const
- kMinTotalTicksForValidInfo = 4;
- (* This is the minimum number of Ticks that we must have taken in order
- to consider the sample valid.
- *)
-
- const
- kTickAccuracyRequirement = 6;
-
- (* gSecondsPerTick div kTickAccuracyRequirement is the bound between
- the current time and the expected time for a Tick to be considered
- accurate. Ticks that are delivered outside of this bound are inaccurate,
- and don't figure in the activity calculations.
- *)
-
- const
- kManualTimeDisplacementValue = 60;
-
- (* kManualTimeDisplacementValue is the amount of time (in minutes)
- that the debugging time displacement commands add or subtract
- to the current time.
- *)
-
- (* ----- Layout Constants ----- *)
-
- const
- kSourceHeight = 34;
- (* The scrolling display is this many pixels high. *)
-
- kGWorldHExtra = 80;
- (* The number of horizontal pixels that the offscreen GWorld
- is bigger than the onscreen display. This allows us
- to draw off into the offscreen buffer off the edge of the
- visible display, and smooth scroll the results on to the
- screen.
- *)
-
- kDestHOffset = 0;
- hDestVOffset = 2;
- (* When drawn in the window, the scrolling display has a border
- of this many pixels at each edge.
- *)
-
- kGraphVOffset = 18;
- (* The number of pixels from the top of the scrolling display
- to the base of the activity graph.
- *)
-
- kIconHOffset = -4;
- kIconVOffset = 26;
- (* The offset from the base of the right hand side of the activty
- graph to the location day/night icons.
- *)
-
- kIconSuiteToDayLabelHOffset = 8;
- kIconSuiteToDayLabelVOffset = 9;
- (* The offset from the bottom right of the night icon to
- the place where we start drawing the day labels.
- *)
-
- kDefaultWindowFromRight = 60;
- kDefaultWindowFromBottom = 4;
- (* If we don't have a saved window position, we put the window
- at this offset from the bottom right of the main display.
- *)
-
- kOffscreenGWorldBitDepth = 8;
- (* The pixel depth of the offscreen GWorld. *)
-
- kLabelFontSize = 9;
- (* The font size used to draw the day labels. *)
-
- (* ----- Global Data Structures ----- *)
-
- const
- kActivityMin = 0;
- kActivityMax = kTicksPerTock;
- type
- ActivityValueRange = kActivityMin..kActivityMax;
-
- (* ActivityValueRange defines the range that activity values can fall in to.
- Because there are only kTicksPerTock Ticks in a Tock, it stands to reason
- that the maximum number of activity samples we can have in a Tock is
- kTicksPerTock.
- *)
-
- type
- ActivityLogType =
- record
- lastTock : DateTimeRec;
- recentActivity : array [0..kNumberOfTocks - 1] of
- record
- activityTicks : integer;
- totalTicks : integer;
- end;
- end;
- ActivityLogPtr = ^ActivityLogType;
- ActivityLogHandle = ^ActivityLogPtr;
-
- (* ActivityLogType is the type used to store the recent activity. Each entry
- in recentActivity is the activity values for a particular Tock. Entries
- with small indices preceed entries with greater indices in real time.
- lastTock is the real time that the most recent entry was sampled.
- *)
-
- (* ----- General Program Variables ----- *)
-
- var
- gQuitNow : Boolean; (* Setting this to true exits the main event loop. *)
-
- gMainWindow : WindowPtr; (* The main display window. *)
-
- gOffscreenActivityGWorld : GWorldPtr;
- (* A buffer used to hold the contents of gMainWindow for
- quick refresh.
- *)
-
- gLastMousePos : Point; (* The last mouse position we saw -- used to detect
- movement and hence user activity.
- *)
-
- (* ----- Layout Variables ----- *)
-
- var
- gGWorldRect : Rect; (* The bounds of gOffscreenActivityGWorld. *)
-
- gSourceRect : Rect; (* The source rect used when updating gMainWindow from
- gOffscreenActivityGWorld. Different from gGWorldRect
- because gGWorldRect has kGWorldHExtra extra horizontal
- pixels to allow for smooth scrolling on to the screen.
- *)
-
- gDestRect : Rect; (* The destination rect used when updating gMainWindow from
- gOffscreenActivityGWorld. The Grow and Shrink commands
- bash this rectangle, and CopyBits does the pixel
- expansion work for us.
- *)
-
- gExpansionFactor : integer; (* The Grow and Shrink commands record the current
- expansion factor here.
- *)
-
- gActiveValueColourMap : array [ActivityValueRange] of RGBColor;
- (* Maps activity values into an appropriate colour
- We pull this in from a resource to allow for
- tweaking.
- *)
-
- gSunIconSuite : Handle; (* An icon suite handle that holds the day icon. *)
- gMoonIconSuite : Handle; (* An icon suite handle that holds the night icon. *)
-
- (* ----- Global Variables ----- *)
-
- var
- gActivityLog : ActivityLogType; (* The global record of recent activity. *)
-
- gNeedsSaving : Boolean; (* Set to true when gActivityLog needs saving to disk,
- cleared when it is saved. This prevents multiple
- sequential Tocks (such as those that happen when
- time is discontinuous) from hammering the disk.
- It also allows us to defer saving until the
- hard disk is spun up on a PowerBook.
- *)
-
- (* ----- Time Variables ----- *)
-
- var
- gTimeOffset : longint; (* The number of seconds to add to the current dateTime
- to yield the virtual dateTime. This allows the
- time distortion debugging commands to distort
- time simply.
- *)
-
- gTotalTicks : integer; (* Records the number of ticks we've done since
- the most recent Tock.
- *)
-
- gActivityTicks : integer; (* Records the number of ticks since the last Tock
- that we've seen user activity.
- *)
-
- gExpectedTickTime : longint; (* The dateTime for the next expected Tick.
- *)
-
- (* ***** Simply Application-Specific Routines ***** *)
-
- function GetHourField(dateTime : DateTimeRec) : integer;
- (* An accessor for the hour field of the DateTimeRec. We
- use an accessor to allow for accelerated time during
- debugging.
- *)
- begin
- if gDebugging then begin
- GetHourField := dateTime.minute;
- end else begin
- GetHourField := dateTime.hour;
- end; (* if *)
- end; (* GetHourField *)
-
- function GetMinuteField(dateTime : DateTimeRec) : integer;
- (* An accessor for the minute field of the DateTimeRec. We
- use an accessor to allow for accelerated time during
- debugging.
- *)
- begin
- if gDebugging then begin
- GetMinuteField := dateTime.second;
- end else begin
- GetMinuteField := dateTime.minute;
- end; (* if *)
- end; (* GetMinuteField *)
-
- procedure SetMinuteField(var dateTime : DateTimeRec; minute : integer);
- (* An accessor for the minute field of the DateTimeRec. We
- use an accessor to allow for accelerated time during
- debugging.
- *)
- begin
- if gDebugging then begin
- dateTime.second := minute;
- end else begin
- dateTime.minute := minute;
- end; (* if *)
- end; (* SetMinuteField *)
-
- procedure QAssert(mustBeTrue : Boolean);
- (* Standard assertion routine. We never expect to trip any assertions
- in this program. If we do this routine quits the program in
- a very abrupt manner (only when running the non-debug version of
- course). Note that if this assert trips we bleed a DeathNMRec
- in the system heap. I don't consider this a big issues, simply
- because it should never happen. Even if it does happen, it's
- unlikely you'll be doing this enough to notice the bleed.
- *)
- type
- DeathNMRec =
- record
- noteRec : NMRec;
- text : Str255;
- end;
- DeathNMRecPtr = ^DeathNMRec;
- var
- deathNote : DeathNMRecPtr;
- junk : OSErr;
- begin
- if not mustBeTrue then begin
- if gDebugging then begin
- DebugStr('QAssert: Assert failed.');
- end else begin
- deathNote := DeathNMRecPtr( NewPtrSysClear( sizeof(DeathNMRec)) );
- if deathNote <> nil then begin
- deathNote^.noteRec.qType := ord(nmType);
- deathNote^.noteRec.nmStr := @deathNote^.text;
-
- GetIndString(deathNote^.text, rMiscStrings, strDeath);
- if deathNote^.text = '' then begin
- deathNote^.text := 'The application “Working’ Too Hard” encountered an unexpected error and quit.';
- end; (* if *)
-
- junk := NMInstall(NMRecPtr(deathNote));
- end; (* if *)
- ExitToShell;
- end; (* if *)
- end; (* if *)
- end; (* QAssert *)
-
- function HardDiskSpinning : Boolean;
- (* Returns true if the hard disk is currently powered up.
- Note that this routine assumes the hard disk is powered
- if there's no Power Manager available.
- *)
- var
- result : Boolean;
- response : longint;
- begin
- result := true;
- if Gestalt(gestaltPowerMgrAttr, response) = noErr then begin
- if btst(response, gestaltPMgrDispatchExists) then begin
- if PMSelectorCount >= 6 then begin
- result := HardDiskPowered;
- end; (* if *)
- end; (* if *)
- end; (* if *)
- HardDiskSpinning := result;
- end; (* HardDiskSpinning *)
-
- function MyGetDateTime : longint;
- (* A simple wrapper around the system GetDateTime routine.
- It adds gTimeOffset to the result to allow for
- discontinuous time during debugging.
- *)
- var
- tmp : longint;
- begin
- GetDateTime(tmp);
- tmp := tmp + gTimeOffset;
- MyGetDateTime := tmp;
- end; (* MyGetDateTime *)
-
- procedure SetupExpectedTickTime;
- (* Sets up gExpectedTickTime to point to be the dateTime of the next
- minute boundary. Actually, it uses a two minute delay, to avoid
- having to worry about the current date time wrapper while we're
- modifying tmpDateTime.
- *)
- var
- tmpDatetime : DateTimeRec;
- begin
- gExpectedTickTime := MyGetDateTime;
- SecondsToDate(gExpectedTickTime, tmpDatetime);
- tmpDatetime.second := 0;
- SetMinuteField(tmpDatetime, GetMinuteField(tmpDatetime) + 2);
- DateToSeconds(tmpDateTime, gExpectedTickTime);
- end; (* SetupExpectedTickTime *)
-
- procedure CalculateWindowRect(var windowRect : Rect);
- (* Given the basic destination rectangle size specified by windowRect
- (whose topLeft co-ordinate must be 0,0), calculate the gDestRect
- and return windowRect as the rectangle of the enclosing window
- (ie around gDestRect and the border around it).
- *)
- begin
- gDestRect := windowRect;
- OffsetRect(gDestRect, kDestHOffset, hDestVOffset);
-
- InsetRect(windowRect, -kDestHOffset, -hDestVOffset);
-
- OffsetRect(windowRect,
- qd.screenBits.bounds.right - (windowRect.right - windowRect.left + kDefaultWindowFromRight),
- qd.screenBits.bounds.bottom - (windowRect.bottom - windowRect.top + kDefaultWindowFromBottom)
- );
- end; (* CalculateWindowRect *)
-
- function SystemActive : Boolean;
- (* Returns true if the machine is actively being used by a human (or
- something that's close to a human). It detects mouse movements
- by comparing the mouse position to gLastMousePos, which is also
- updates. It also attempt to detect key presses by peeking
- in the OS event queue. The key detection is pretty feeble, and
- I may need to revise this routine to do a better job. [Possible
- a jGNEFilter, but I'll consult with others before doing that.]
- *)
- var
- result : Boolean;
- event : EventRecord;
- currentMousePos : Point;
- junkBool : Boolean;
- begin
- junkBool := OSEventAvail(0, event);
- currentMousePos := event.where;
- result := (currentMousePos.h <> gLastMousePos.h) or (currentMousePos.v <> gLastMousePos.v);
- if not result then begin
- result := OSEventAvail(keyDownMask + keyUpMask + autoKeyMask, event);
- end; (* if *)
- gLastMousePos := currentMousePos;
- SystemActive := result;
- end; (* SystemActive *)
-
- procedure UpdateMainWindow;
- (* Updates the main window from gOffscreenActivityGWorld. *)
- begin
- SetPort(gMainWindow);
- CopyBits( GrafPtr(gOffscreenActivityGWorld)^.portBits, gMainWindow^.portBits,
- gSourceRect, gDestRect, srcCopy, nil);
- end; (* UpdateMainWindow *)
-
- procedure FlushApplicationVolume;
- (* Flushes the volume that contains the application. It does
- this by looking up the FCB for the application's resource
- fork to find the application's volume's vRefNum.
- *)
- var
- fcbPB : FCBPBRec;
- junkName : Str255;
- begin
- fcbPB.ioNamePtr := @junkName;
- fcbPB.ioVRefNum := 0;
- fcbPB.ioRefNum := CurResFile;
- fcbPB.ioFCBIndx := 0;
- QAssert( PBGetFCBInfoSync(@fcbPB) = noErr);
- QAssert( FlushVol(nil, fcbPB.ioVRefNum) = noErr);
- end; (* FlushApplicationVolume *)
-
- (* ***** Core Implementation ***** *)
-
- procedure SaveActivityLog;
- (* Saves gActivityLog to a resource in the application's resource fork. *)
- var
- savedActivity : Handle;
- begin
- savedActivity := Get1Resource('PRef', rSavedActivity);
- if savedActivity = nil then begin
- savedActivity := NewHandle(sizeof(gActivityLog));
- QAssert( savedActivity <> nil );
- AddResource(savedActivity, 'PRef', rSavedActivity, '');
- end else begin
- SetHandleSize(savedActivity, sizeof(gActivityLog));
- end; (* if *)
-
- QAssert(savedActivity <> nil);
- QAssert( GetHandleSize(savedActivity) = sizeof(gActivityLog) );
-
- BlockMoveData(@gActivityLog, savedActivity^, sizeof(gActivityLog));
- ChangedResource(savedActivity);
- UpdateResFile(CurResFile);
- FlushApplicationVolume;
-
- gNeedsSaving := false;
- end; (* SaveActivityLog *)
-
- procedure ClearActivityLog;
- (* Clears the global activity log, and refreshes the display. *)
- var
- oldWorld : GrafPtr;
- tmpStr : Str255;
- i : integer;
- begin
- GetPort(oldWorld);
-
- (* First, clear the offscreen world, and write an explanatory message. *)
- SetPort(GrafPtr(gOffscreenActivityGWorld));
- EraseRect(gGWorldRect);
- GetIndString(tmpStr, rMiscStrings, strTimeDiscontinuity);
- TextFace([italic]);
- TETextBox(@tmpStr[1], length(tmpStr), gSourceRect, teJustRight);
- TextFace([]);
-
- (* Now force an update of the onscreen world. *)
- SetPort(gMainWindow);
- InvalRect(gDestRect);
- SetPort(oldWorld);
-
- (* Finally, clear gActivityLog. *)
- for i := 0 to kNumberOfTocks - 1 do begin
- gActivityLog.recentActivity[i].activityTicks := 0;
- gActivityLog.recentActivity[i].totalTicks := 0;
- end; (* for *)
- SetupExpectedTickTime;
-
- gNeedsSaving := true;
- end; (* ClearActivityLog *)
-
- procedure Tock(activityTicks : integer; totalTicks : integer; tockTime : DateTimeRec; redraw : Boolean);
- (* Tock is called every fifteen minutes and represents the true core of
- the implementation. The activityTicks and totalTicks represent the
- data to be logged for this Tock. The tockTime parameter is the time
- at which the Tock happened (or was scheduled to happen), as distinct
- from the current time. The redraw parameter tells the routine whether
- to redraw immediately or not.
- Some non-obvious things:
-
- o I update the on-screen image by copying the entire offscreen image.
- I could call ScrollRect on the on-screen image, but this is tricky
- if there is a pending update event. Anyway, ScrollRect is simply
- a special case for CopyBits, so I might as well CopyBits it from
- offscreen -- it may even be faster (because it's non-overlapping).
-
- o The marker drawing code looks for (minute = kTicksPerTock), rather
- than (minute = 0). This is because when we're called at (minute = 0),
- the actual data (ie activityTicks and totalTicks) is for the previous
- fifteen minutes. Rather than mess with the calling sequence, I adjust
- for it here.
- *)
- var
- i : integer;
- activityInfoValid : Boolean;
- scaledActivity : integer;
- oldWorld : GrafPtr;
- junkRgn : RgnHandle;
- oldColour : RGBColor;
- sicnRect : Rect;
- minute : integer;
- hour : integer;
- begin
- {$ifc false}
- DebugStr(stringof('Tock: activityTicks = ', activityTicks:1, ', totalTicks = ', totalTicks:1, ' ; g'));
- {$endc}
- gNeedsSaving := true;
-
- (* Log the raw activity information. *)
- for i := 1 to kNumberOfTocks - 1 do begin
- gActivityLog.recentActivity[i - 1] := gActivityLog.recentActivity[i];
- end; (* for *)
- gActivityLog.recentActivity[kNumberOfTocks - 1].activityTicks := activityTicks;
- gActivityLog.recentActivity[kNumberOfTocks - 1].totalTicks := totalTicks;
- gActivityLog.lastTock := tockTime;
-
- (* Process the activity information to yield scaledActivity, which
- will be the height of the graph for this tock.
- *)
- activityInfoValid := (totalTicks >= kMinTotalTicksForValidInfo);
- if activityInfoValid then begin
- scaledActivity := (activityTicks * kActivityMax) div totalTicks;
- end else begin
- scaledActivity := 0;
- end; (* if *)
- QAssert( (scaledActivity >= kActivityMin) & (scaledActivity <= kActivityMax) );
-
- junkRgn := NewRgn;
- QAssert(junkRgn <> nil);
- GetPort(oldWorld);
-
- (* Switch to the offscreen world, and scroll it left one pixel. *)
- SetPort(GrafPtr(gOffscreenActivityGWorld));
- ScrollRect(gGWorldRect, -1, 0, junkRgn);
-
- (* Now draw any new info in the offscreen world. *)
- GetForeColor(oldColour);
- if activityInfoValid then begin
-
- (* Draw the base line. *)
- MoveTo(gSourceRect.right - 1, kGraphVOffset);
- Line(0, 0);
-
- (* Draw the bar height. *)
- RGBForeColor( gActiveValueColourMap[scaledActivity] );
- MoveTo(gSourceRect.right - 1, kGraphVOffset - 1);
- Line(0, -scaledActivity);
- end; (* if *)
- RGBForeColor(oldColour);
-
- hour := GetHourField(tockTime);
- minute := GetMinuteField(tockTime);
-
- (* Now draw the hour marker. *)
- if minute = kTicksPerTock then begin
- MoveTo(gSourceRect.right - 1, kGraphVOffset + 1);
- Line(0, 1);
- end; (* if *)
-
- (* Now draw the day marker. *)
- if ((hour = 0) or (hour = 12)) and (minute = kTicksPerTock) then begin
- MoveTo(gSourceRect.right - 1, kGraphVOffset + 3);
- Line(0, 1);
- end; (* if *)
-
- (* Draw the day/night icons. *)
- if ((hour = 0) or (hour = 12)) and (minute = kTicksPerTock) then begin
- SetRect(sicnRect, 0, 0, 16, 16);
- OffsetRect(sicnRect, gSourceRect.right + kIconHOffset, kIconVOffset);
- if hour = 0 then begin
- QAssert( PlotIconSuite(sicnRect, kAlignNone, kTransformNone, gMoonIconSuite) = noErr);
- MoveTo(sicnRect.right - kIconSuiteToDayLabelHOffset, sicnRect.bottom - kIconSuiteToDayLabelVOffset);
- DrawString(GetShortDayName(tockTime.dayOfWeek));
- end else begin
- QAssert( PlotIconSuite(sicnRect, kAlignNone, kTransformNone, gSunIconSuite) = noErr);
- end; (* if *)
- end; (* if *)
-
- (* If this is an immediate operation, redraw the main window from the offscreen GWorld. *)
- if redraw then begin
- UpdateMainWindow;
- ValidRect(gDestRect);
- end; (* if *)
-
- (* Clean up. *)
- SetPort(oldWorld);
- DisposeRgn(junkRgn);
- end; (* Tock *)
-
- procedure Tick(currentTime : longint);
- (* This routine is called every item the currentTime exceeds gExpectedTickTime. The
- routine also updates gExpectedTickTime by gSecondsPerTick. The net effect is that
- this routine is called roughly every minute.
- *)
- var
- tmpDateRec : DateTimeRec;
- begin
- (* Collect activity statistics. If currentTime is close enough to gExpectedTickTime,
- we note that we have a new sample by bumping gTotalTicks. If the system is
- active, we also bump gActivityTicks.
- *)
- if abs(currentTime - gExpectedTickTime) < ((gSecondsPerTick div kTickAccuracyRequirement) + 1) then begin
- if SystemActive then begin
- gActivityTicks := gActivityTicks + 1;
- end; (* if *)
- gTotalTicks := gTotalTicks + 1;
- end; (* if *)
-
- (* Now check whether we've hit a fifteen minute boundary. If so,
- call Tock and then reset the counters.
- *)
- SecondsToDate(gExpectedTickTime, tmpDateRec);
- if (GetMinuteField(tmpDateRec) mod kTicksPerTock) = 0 then begin
- Tock(gActivityTicks, gTotalTicks, tmpDateRec, true);
- gTotalTicks := 0;
- gActivityTicks := 0;
- end; (* if *)
-
- (* Finally bump gExpectedTickTime, so that we get called back next minute. *)
- gExpectedTickTime := gExpectedTickTime + gSecondsPerTick;
- end; (* Tick *)
-
- (* ***** AppleEvent Handling ***** *)
-
- function HandleAEOpenApplication (var theAppleEvent : AppleEvent; var reply: AppleEvent; refcon : longint): OSErr;
- (* Handle the Open Application AppleEvent, which does not a lot.
- *)
- var
- err: OSStatus;
- begin
- {$unused reply}
- {$unused refcon}
- err := AEGotRequiredParams(theAppleEvent);
- if err = noErr then begin
- err := noErr;
- end; (* if *)
- HandleAEOpenApplication := err;
- end; (* HandleAEOpenApplication *)
-
- function HandleAEQuitApplication (var theAppleEvent, reply: AppleEvent; refcon : longint): OSErr;
- (* Handle the Quit AppleEvent. Simple sets gQuitNow.
- *)
- var
- err: OSStatus;
- begin
- {$unused reply}
- {$unused refcon}
- err := AEGotRequiredParams(theAppleEvent);
- if err = noErr then begin
- gQuitNow := true;
- end; (* if *)
- HandleAEQuitApplication := err;
- end; (* HandleAEQuitApplication *)
-
- (* ***** Menu Handling ***** *)
-
- procedure AdjustMenu;
- (* Adjust the various menus in preparation for a handling a menu operation.
- Note that this deliberately doesn't adjust the Debug menu because that
- menu is for debugging, and I don't really care whether it's adjusted
- properly.
- *)
- var
- editMenuH : MenuHandle;
- fileMenuH : MenuHandle;
- begin
- (* Edit *)
- editMenuH := GetMenuHandle(mEdit);
- QAssert( editMenuH <> nil );
- if FrontWindow = gMainWindow then begin
- DisableItem(editMenuH, iUndo);
- DisableItem(editMenuH, iCut);
- EnableItem(editMenuH, iCopy);
- DisableItem(editMenuH, iPaste);
- DisableItem(editMenuH, iClear);
- end else begin
- EnableItem(editMenuH, iUndo);
- EnableItem(editMenuH, iCut);
- EnableItem(editMenuH, iCopy);
- EnableItem(editMenuH, iPaste);
- EnableItem(editMenuH, iClear);
- end; (* if *)
-
- (* File *)
- fileMenuH := GetMenuHandle(mFile);
- QAssert( fileMenuH <> nil );
- if gNeedsSaving then begin
- EnableItem(fileMenuH, iSave);
- end else begin
- DisableItem(fileMenuH, iSave);
- end; (* if *)
- end; (* AdjustMenu *)
-
- procedure DoMenu(menuItem : longint);
- (* Act is response to a menu command. Most of this is standard menu handling
- stuff, with a few real operations mixed in.
- *)
- var
- menu : integer;
- item : integer;
- daName : Str255;
- junk : integer;
- windowRect : Rect;
- newPict : PicHandle;
- junkLong : longint;
- versH : VersRecHndl;
- startTicks : longint;
- begin
- startTicks := TickCount;
- menu := hiwrd(menuItem);
- item := lowrd(menuItem);
- case menu of
- mApple:
- if item = iAbout then begin
- versH := VersRecHndl(Get1Resource('vers', 1));
- QAssert(versH <> nil);
- ParamText(versH^^.shortVersion, '', '', '');
- junk := Alert(rAboutAlert, nil);
- end else begin
- GetMenuItemText(GetMenuHandle(mApple), item, daName);
- junk := OpenDeskAcc(daName);
- end; (* if *)
- mFile:
- case item of
- iSave:
- begin
- QAssert( gNeedsSaving );
- SaveActivityLog;
- end;
- iQuit:
- gQuitNow := true;
- otherwise
- (* do nothing *) ;
- end; (* case *)
- mEdit:
- case item of
- iCopy :
- begin
- (* Create a picture of the main window, and put it into the scrap. *)
- SetPort(gMainWindow);
- newPict := OpenPicture(gDestRect);
- QAssert( newPict <> nil );
- UpdateMainWindow;
- ClosePicture;
- junkLong := ZeroScrap;
- HLock(Handle(newPict));
- junkLong := PutScrap(GetHandleSize(Handle(newPict)), 'PICT', newPict^);
- KillPicture(newPict);
- end;
- otherwise
- (* do nothing *) ;
- end; (* case *)
- mDebug:
- case item of
- iGrow, iShrink:
- begin
- (* Grow or shrink the main window, adjusting gDestRect so that
- the offscreen world is stretched when it's redrawn.
- *)
- if item = iGrow then begin
- gExpansionFactor := gExpansionFactor * 2;
- end else begin
- gExpansionFactor := gExpansionFactor div 2;
- if gExpansionFactor < 1 then begin
- gExpansionFactor := 1;
- end; (* if *)
- end; (* if *)
- windowRect := gSourceRect;
- windowRect.top := windowRect.top * gExpansionFactor;
- windowRect.bottom := windowRect.bottom * gExpansionFactor;
- windowRect.left := windowRect.left * gExpansionFactor;
- windowRect.right := windowRect.right * gExpansionFactor;
-
- CalculateWindowRect(windowRect);
-
- SizeWindow(gMainWindow,
- windowRect.right - windowRect.left,
- windowRect.bottom - windowRect.top,
- false);
-
- (* If the window gets too big, place it on the screen so we can
- see the right hand side.
- *)
- if (windowRect.right - windowRect.left) > (qd.screenBits.bounds.right - qd.screenBits.bounds.left) then begin
- MoveWindow(gMainWindow, qd.screenBits.bounds.right - (windowRect.right - windowRect.left + 20) - 4, 40, true);
- end; (* if *)
-
- SetPort(gMainWindow);
- EraseRect(WindowPeek(gMainWindow)^.port.portRect);
- InvalRect(gDestRect);
- end;
- iTimeBackward:
- gTimeOffset := gTimeOffset - (gSecondsPerTick * kManualTimeDisplacementValue);
- iTimeForward:
- gTimeOffset := gTimeOffset + (gSecondsPerTick * kManualTimeDisplacementValue);
- iFail:
- begin
- gDebugging := false;
- QAssert(false);
- end
- otherwise
- (* do nothing *) ;
- end; (* case *)
- otherwise
- (* do nothing *)
- end; (* case *)
-
- if not gQuitNow then begin
- (* Make sure that all menu commands (except Quit) take at least 10 ticks,
- so that the user can actually see the menu title flash when using
- command keys.
- *)
- while TickCount < startTicks + 10 do begin
- Delay(1, junkLong);
- end; (* while *)
- HiliteMenu(0);
- end; (* if *)
- end; (* DoMenu *)
-
- (* ***** Low-Level Event Handling ***** *)
-
- procedure DoDragWindow(hitWindow : WindowPtr; event : EventRecord);
- (* Respond to a mouse down inDrag or inContent. Calls DragWindow
- then saves then remembers the position in a resource.
- *)
- var
- currentPosition : Rect;
- windowPos : Handle;
- begin
- DragWindow(hitWindow, event.where, qd.screenBits.bounds);
- if hitWindow = gMainWindow then begin
- GetWindowRect(gMainWindow, currentPosition);
- windowPos := Get1Resource('PRef', rWindowPosition);
- if windowPos = nil then begin
- windowPos := NewHandle(sizeof(Point));
- QAssert(windowPos <> nil);
- AddResource(windowPos, 'PRef', rWindowPosition, '');
- end else begin
- SetHandleSize(windowPos, sizeof(Point));
- end; (* if *)
- PointPtr(windowPos^)^ := currentPosition.topLeft;
- ChangedResource(windowPos);
- UpdateResFile(CurResFile);
- FlushApplicationVolume;
- end; (* if *)
- end; (* DoDragWindow *)
-
- procedure HandleEvent(event : EventRecord);
- (* Handle a user interface event. Standard event loop fodder. *)
- var
- hitWindow : WindowPtr;
- junk : OSErr;
- begin
- case event.what of
- updateEvt:
- begin
- hitWindow := WindowPtr(event.message);
- BeginUpdate(hitWindow);
- if hitWindow = gMainWindow then begin
- UpdateMainWindow;
- end; (* if *)
- EndUpdate(hitWindow);
- end;
- mouseDown:
- case FindWindow(event.where, hitWindow) of
- inMenuBar:
- begin
- AdjustMenu;
- DoMenu(MenuSelect(event.where));
- end;
- inDrag, inContent:
- DoDragWindow(hitWindow, event);
- otherwise
- (* do nothing *)
- end; (* case *)
- keyDown:
- if band(event.modifiers, cmdKey) <> 0 then begin
- AdjustMenu;
- DoMenu(MenuKey(chr(band(event.message, charCodeMask))));
- end else begin
- SysBeep(10);
- end; (* if *)
- nullEvent:
- (* do nothing *) ;
- kHighLevelEvent:
- junk := AEProcessAppleEvent(event);
- end; (* case *)
- end; (* HandleEvent *)
-
- (* ***** Initialisation ***** *)
-
- procedure InitApplication;
- (* Initialise all the application's global state. Lots of code here! *)
- var
- response : longint;
- sysEnv : SysEnvRec;
- junk : integer;
- versH : VersRecHndl;
- km : KeyMap;
- rgbResource : Handle;
- windowRect : Rect;
- mbar : Handle;
- debugMenu : MenuHandle;
- oldWorld : GrafPtr;
- windowPos : Handle;
- savedPosition : Point;
- originalPosition : Rect;
- tmpStr : Str255;
- savedActivity : Handle;
- tmpDatetime : DateTimeRec;
- i : integer;
- begin
- (* Check Environment -- Make sure we have System 7 or greater and Colour QuickDraw. *)
- if (Gestalt(gestaltSystemVersion, response) <> noErr) |
- (response < $0700) |
- (SysEnvirons(curSysEnvVers, sysEnv) <> noErr) |
- not sysEnv.hasColorQD then begin
- junk := StopAlert(rInsufficientlyCoolAlert, nil);
- ExitToShell;
- end; (* if *)
-
- (* Debugging -- Set gDebugging to true, unless the 'vers' resource indicates
- that this is a final version. Toggle whatever setting we have if the option
- key is down.
- *)
-
- gDebugging := true;
- versH := VersRecHndl(Get1Resource('vers', 1));
- if versH <> nil then begin
- gDebugging := (versH^^.numericVersion.stage < betaStage);
- end; (* if *)
- GetKeys(km);
- if km[kOptionKeyCode] then begin
- gDebugging := not gDebugging;
- end; (* if *)
-
- (* 'Constants' -- Set up gSecondsPerTick for either fast or slow time
- depending on whether we're debugging or not.
- *)
-
- if gDebugging then begin
- gSecondsPerTick := kDebuggingSecondsPerTick;
- end else begin
- gSecondsPerTick := kStandardSecondsPerTick;
- end; (* if *)
-
- (* Layout -- Set up all the rectangles needed to draw the graph,
- along with some constant data structures for icons and such.
- *)
-
- gSourceRect.top := 0;
- gSourceRect.left := 0;
- gSourceRect.bottom := kSourceHeight;
- gSourceRect.right := kNumberOfTocks;
-
- windowRect := gSourceRect;
- CalculateWindowRect(windowRect); (* Also sets up gDestRect. *)
-
- gGWorldRect := gSourceRect;
- gGWorldRect.right := gGWorldRect.right + kGWorldHExtra;
-
- gExpansionFactor := 1;
-
- rgbResource := Get1Resource('RGB#', rColourMap);
- QAssert( rgbResource <> nil);
- QAssert( GetHandleSize(rgbResource) = sizeof(gActiveValueColourMap) );
- BlockMoveData(rgbResource^, @gActiveValueColourMap, sizeof(gActiveValueColourMap));
-
- QAssert( GetIconSuite(gSunIconSuite, rSunIcon, kSelectorAllSmallData) = noErr);
- QAssert( gSunIconSuite <> nil);
- QAssert( GetIconSuite(gMoonIconSuite, rMoonIcon, kSelectorAllSmallData) = noErr);
- QAssert( gMoonIconSuite <> nil);
-
- (* Menus -- Create the menu bar. *)
-
- mbar := GetNewMBar(rMainMenuBar);
- QAssert(mbar <> nil);
- SetMenuBar(mbar);
- if gDebugging then begin
- debugMenu := GetMenu(mDebug);
- QAssert( debugMenu <> nil );
- InsertMenu(debugMenu, 0);
- end; (* if *)
- AppendResMenu(GetMenuHandle(mApple), 'DRVR');
- DrawMenuBar;
-
- (* AppleEvent Handlers -- Install our AppleEvent handlers. *)
-
- QAssert( AEInstallEventHandler(kCoreEventClass, kAEOpenApplication,
- NewAEEventHandlerProc(HandleAEOpenApplication), 0, false) = noErr);
- QAssert( AEInstallEventHandler(kCoreEventClass, kAEQuitApplication,
- NewAEEventHandlerProc(HandleAEQuitApplication), 0, false) = noErr);
-
- (* General -- Set up the general variables, including creating the window
- and offscreen buffer.
- *)
-
- gQuitNow := false;
- gMainWindow := NewCWindow(nil, windowRect, '', false, altDBoxProc, WindowPtr(-1), false, 0);
- QAssert(gMainWindow <> nil);
-
- (* Restore the window position from a resource. Move it back on screnn if
- necessary.
- *)
- windowPos := Get1Resource('PRef', rWindowPosition);
- if windowPos <> nil then begin
- savedPosition := PointPtr(windowPos^)^;
-
- GetWindowRect(gMainWindow, originalPosition);
- MoveWindow(gMainWindow, savedPosition.h, savedPosition.v, false);
- ShowWindow(gMainWindow);
- if not TitleBarOnScreen(gMainWindow) then begin
- MoveWindow(gMainWindow, originalPosition.left, originalPosition.top, false);
- end; (* if *)
- end else begin
- ShowWindow(gMainWindow);
- end; (* if *)
-
- (* Create and set up the offscreen GWorld. *)
- QAssert( NewGWorld(gOffscreenActivityGWorld, kOffscreenGWorldBitDepth, gGWorldRect, nil, nil, 0) = noErr);
- GetPort(oldWorld);
- SetPort(GrafPtr(gOffscreenActivityGWorld));
- EraseRect(gGWorldRect);
- TextFont(applFont);
- TextSize(kLabelFontSize);
-
- gLastMousePos.h := -1;
- gLastMousePos.v := -1;
-
- (* Time *)
-
- gActivityTicks := 0;
- gTotalTicks := 0;
- gTimeOffset := 0;
-
- (* Global State *)
-
- (* Get the saved info out of a resource. *)
- savedActivity := Get1Resource('PRef', rSavedActivity);
- if savedActivity <> nil then begin
- if GetHandleSize(savedActivity) = sizeof(gActivityLog) then begin
- HLock(savedActivity);
- end else begin
- savedActivity := nil;
- end; (* if *)
- end; (* if *)
-
- (* Initialise the activity log in one of two ways, depending on whether
- we have saved info or not.
- *)
- if savedActivity = nil then begin
- (* Start time anew -- relies on the fact that gActivityLog is initialised to zero
- by the environment.
- *)
- SetupExpectedTickTime;
- GetIndString(tmpStr, rMiscStrings, strPatience);
- TextFace([italic]);
- TETextBox(@tmpStr[1], length(tmpStr), gSourceRect, teJustLeft);
- TextFace([]);
- end else begin
- (* Use the existing activity log. This is a really horrible chunk of code.
- The basic problem is that we have to feed data into the gActivityLog,
- however we can't just overwrite it because then we'd have to redraw
- the stuff in the offscreen world. So instead we feed the data into
- the activity log one sample at a time by calling Tock.
- To do this, we need to calculate the time associated with each sample.
- However we only remember the time of the last sample (ie lastTock),
- so we have to calculate the time for each of the previous sample
- by extrapolating back from lastTock. Fortunately we know that each
- Tock is separated by 15 minutes, so we can work out the time of
- the first tock.
- *)
-
- (* Calculate the time of the first saved Tock by converting lastTock to
- seconds, then subtracting away fifteen minutes for each kNumberOfTocks.
- *)
- DateToSeconds( ActivityLogHandle(savedActivity)^^.lastTock, gExpectedTickTime);
- gExpectedTickTime := gExpectedTickTime - (longint(kNumberOfTocks) * kTicksPerTock * gSecondsPerTick);
-
- (* Now loop through each saved sample, calling Tock for each one. *)
- for i := 0 to kNumberOfTocks - 1 do begin
- SecondsToDate(gExpectedTickTime, tmpDatetime);
- Tock( ActivityLogHandle(savedActivity)^^.recentActivity[i].activityTicks,
- ActivityLogHandle(savedActivity)^^.recentActivity[i].totalTicks,
- tmpDatetime, false
- );
- (* Advance gExpectedTickTime by a Tock's worth of time. *)
- gExpectedTickTime := gExpectedTickTime + kTicksPerTock * gSecondsPerTick;
- end; (* for *)
-
- (* Now move gExpectedTickTime back 14 minutes, so that we can start afresh. *)
- gExpectedTickTime := gExpectedTickTime - (kTicksPerTock - 1) * gSecondsPerTick;
-
- SetPort(gMainWindow);
- InvalRect(gDestRect);
-
- {$ifc false}
- DebugStr(stringof('gExpectedTickTime = ', DateTimeToString(gExpectedTickTime)));
- {$endc}
- end; (* if *)
-
- (* Clean up. *)
- if savedActivity <> nil then begin
- HUnlock(savedActivity);
- end; (* if *)
- SetPort(oldWorld);
- end; (* InitApplication *)
-
- var
- junkBool : Boolean;
- event : EventRecord;
- currentTime : longint;
- delay : longint;
- begin
- (* Init the universe. *)
- InitToolbox;
- InitApplication;
-
- (* The main event loop! *)
- repeat
-
- (* Calculate delay, the number of ticks to between the current time and the
- time for the next tick. This is what we pass as the sleep parameter
- to WaitNextEvent.
- *)
- delay := (gExpectedTickTime - MyGetDateTime) * 60;
- if delay < 0 then begin
- delay := 0;
- end; (* if *)
- junkBool := WaitNextEvent(everyEvent, event, delay, nil);
-
- HandleEvent(event);
-
- (* Check whether something completely wacky has happened to the current time.
- If so, clear the activity graph.
- *)
- currentTime := MyGetDateTime;
- if abs(currentTime - gExpectedTickTime) >= (kNumberOfTocks * kTicksPerTock * gSecondsPerTick) then begin
- ClearActivityLog;
- end; (* if *)
-
- (* Handling any pending Ticks. *)
- repeat
- currentTime := MyGetDateTime;
- if currentTime >= gExpectedTickTime then begin
- Tick(currentTime);
- end; (* if *)
- until currentTime < gExpectedTickTime;
-
- (* Save the activity log if necessary. *)
- if gNeedsSaving then begin
- if HardDiskSpinning then begin
- SaveActivityLog;
- end; (* if *)
- end; (* if *)
-
- until gQuitNow;
- end. (* WorkinTooHard *)
-